#include "script.hpp"
#include "fmt.hpp"

namespace sdl_engine::component::script {

/**********************
*  LuaScript
**********************/

LuaScript::LuaScript() {
    L = luaL_newstate();
    luaL_openlibs(L);
}

bool LuaScript::DoFile(const std::string& filename) {
    if (luaL_dofile(L, filename.c_str())) {
        Loge(Fmt("%s executed error:\n%s", filename.c_str(), lua_tostring(L, -1)));
        lua_pop(L, 1);
        return false;
    }
    traceStack_.clear();
    nest_ = 0;
    return true;
}

std::optional<double> LuaScript::GetNumber(const std::string& name) {
    if (isGlobalSpace()) {
        return getGlobalNumber(name);
    } else {
        return getFieldNumber(name);
    }
}

double LuaScript::GetNumberOrValue(const std::string& name, double defaultValue) {
    if (isGlobalSpace()) {
        return getGlobalNumberOrValue(name, defaultValue);
    } else {
        return getFieldNumberOrValue(name, defaultValue);
    }
}

double LuaScript::GetNumberOrError(const std::string& name) {
    if (isGlobalSpace()) {
        return getGlobalNumberOrError(name);
    } else {
        return getFieldNumberOrError(name);
    }
}

bool LuaScript::GetBool(const std::string& name) {
    if (isGlobalSpace()) {
        return getGlobalBool(name);
    } else {
        return getFieldBool(name);
    }
}

std::string LuaScript::GetString(const std::string& name) {
    if (isGlobalSpace()) {
        return getGlobalString(name);
    } else {
        return getFieldString(name);
    }
}

Color LuaScript::GetColor(const std::string& name) {
    if (isGlobalSpace()) {
        return getGlobalColor(name);
    } else {
        return getFieldColor(name);
    }
}

bool LuaScript::StartQueryTable(const std::string& tableName) {
    if (isGlobalSpace()) {
        return startQueryGlobalTable(tableName);
    } else {
        return startQuerySubTable(tableName);
    }
}

void LuaScript::StopQueryTable() {
    if (isGlobalSpace()) {
        return stopQueryGlobalTable();
    } else {
        return stopQuerySubTable();
    }
}

std::optional<double> LuaScript::getGlobalNumber(const std::string& name) {
    lua_getglobal(L, name.c_str());
    int isNum = false;
    auto number = lua_tonumberx(L, -1, &isNum);
    std::optional<double> result = std::nullopt;
    if (isNum) {
        result = number;
    } else {
        auto type = lua_typename(L, lua_type(L, -1));
        Loge(Fmt("lua type mismatch, the type is %s", type));
    }
    lua_pop(L, 1);
    return result;
}

Color LuaScript::getGlobalColor(const std::string& name) {
    Color result;
    startQueryGlobalTable(name); {
        result.r = getFieldNumberOrValue("r", 0);
        result.g = getFieldNumberOrValue("g", 0);
        result.b = getFieldNumberOrValue("b", 0);
        result.a = getFieldNumberOrValue("a", 0);
    } stopQueryGlobalTable();
    return result;
}

Color LuaScript::getFieldColor(const std::string& name) {
    Color result;
    startQuerySubTable(name); {
        result.r = getFieldNumberOrValue("r", 0);
        result.g = getFieldNumberOrValue("g", 0);
        result.b = getFieldNumberOrValue("b", 0);
        result.a = getFieldNumberOrValue("a", 0);
    } stopQuerySubTable();
    return result;

}

double LuaScript::getGlobalNumberOrError(const std::string& name) {
    auto number = getGlobalNumber(name);
    if (number) {
        return number.value();
    } else {
        throw std::runtime_error(Fmt("%s", GetTraceInfo().c_str()));
    }
    return -1;
}

double LuaScript::getGlobalNumberOrValue(const std::string& name, double defaultValue) {
    auto number = getGlobalNumber(name);
    if (!number) return defaultValue;
    else return number.value();
}

bool LuaScript::getGlobalBool(const std::string& name) {
    lua_getglobal(L, name.c_str());
    bool result = lua_toboolean(L, -1);
    lua_pop(L, 1);
    return result;
}

std::string LuaScript::getGlobalString(const std::string& name) {
    lua_getglobal(L, name.c_str());
    const char* str = lua_tostring(L, -1);
    std::string result;
    if (str)
        result = str;
    lua_pop(L, 1);
    return result;
}

bool LuaScript::startQueryGlobalTable(const std::string& tableName) {
    lua_getglobal(L, tableName.c_str());
    if (lua_istable(L, -1)) {
        traceStack_.push_back(tableName);
        incNest();
        return true;
    } else {
        Loge(Fmt("%s type is not table: %s",
                 lua_typename(L, lua_type(L, -1)),
                 lua_tostring(L, -1)));
        lua_pop(L, 1);
        return false;
    }
}

void LuaScript::stopQueryGlobalTable() {
    if (lua_istable(L, -1)) {
        lua_pop(L, 1);
        decNest();
        traceStack_.pop_back();
    } else {
        Loge("the top of stack isn't table!");
    }
}

std::optional<double> LuaScript::getFieldNumber(const std::string& name) {
    lua_pushstring(L, name.c_str());
    lua_gettable(L, -2);
    if (!lua_isnumber(L, -1)) {
        lua_pop(L, 1);
        return std::nullopt;
    } else {
        auto result = lua_tonumber(L, -1);
        lua_pop(L, 1);
        return result;
    }
}

double LuaScript::getFieldNumberOrValue(const std::string& name, double defaultValue) {
    auto num = getFieldNumber(name);
    if (!num) return defaultValue;
    else return num.value();
}

double LuaScript::getFieldNumberOrError(const std::string& name) {
    auto num = getFieldNumber(name);
    if (num) return num.value();
    else throw std::runtime_error(Fmt("%s", GetTraceInfo().c_str()));
    return -1;
}

bool LuaScript::getFieldBool(const std::string& name) {
    lua_pushstring(L, name.c_str());
    lua_gettable(L, -2);
    auto result = lua_toboolean(L, -1);
    lua_pop(L, 1);
    return result;
}

std::string LuaScript::getFieldString(const std::string& name) {
    lua_pushstring(L, name.c_str());
    lua_gettable(L, -2);
    const char* str = lua_tostring(L, -1);
    std::string result;
    if (str) {
        result = str;
    }
    lua_pop(L, 1);
    return result;
}

bool LuaScript::startQuerySubTable(const std::string& name) {
    lua_pushstring(L, name.c_str());
    lua_gettable(L, -2);
    if (!lua_istable(L, -1)) {
        Loge(Fmt("%s type is not table: %s",
                 lua_typename(L, lua_type(L, -1)),
                 lua_tostring(L, -1)));
        lua_pop(L, 1);
        return false;
    } else {
        traceStack_.push_back(name);
        incNest();
        return true;
    }
}

void LuaScript::stopQuerySubTable() {
    if (lua_istable(L, -1)) {
        lua_pop(L, 1);
        traceStack_.pop_back();
        decNest();
    } else {
        Loge("the top of stack isn't table!");
    }
}

std::string LuaScript::GetTraceInfo() const {
    std::string result;
    for (auto& str : traceStack_) {
        result += str + "->";
    }
    return result;
}

LuaScript::~LuaScript() {
    if (L) {
        lua_close(L);
    }
}


/**********************
*  LuaBridge
**********************/

LuaBridge& LuaBridge::DoFile(const std::string& filename) {
    if (!script_.DoFile(filename)) {
        throw std::runtime_error(Fmt("%s load failed", filename.c_str()));
    }
    return *this;
}

LuaBridge& LuaBridge::Get(double& value, const std::string& name) {
    value = script_.GetNumberOrError(name);
    return *this;
}

LuaBridge& LuaBridge::Get(int& value, const std::string& name) {
    double v;
    Get(v, name);
    value = static_cast<int>(v);
    return *this;
}

LuaBridge& LuaBridge::Get(float& value, const std::string& name) {
    double v;
    Get(v, name);
    value = static_cast<float>(v);
    return *this;
}

LuaBridge& LuaBridge::Get(std::string& value, const std::string& name) {
    value = script_.GetString(name);
    return *this;
}

LuaBridge& LuaBridge::Get(bool& value, const std::string& name) {
    value = script_.GetBool(name);
    return *this;
}

LuaBridge& LuaBridge::Get(Color& color, const std::string& name) {
    color = script_.GetColor(name);
    return *this;
}

LuaBridge& LuaBridge::StartTable(const std::string& table) {
    script_.StartQueryTable(table);
    return *this;
}

LuaBridge& LuaBridge::EndTable() {
    script_.StopQueryTable();
    return *this;
}

}
